# -*- coding: utf-8 -*-
"""
biosppy.utils
-------------

This module provides several frequently used functions and hacks.

:copyright: (c) 2015-2018 by Instituto de Telecomunicacoes
:license: BSD 3-clause, see LICENSE for more details.
"""

# Imports
# compat
from __future__ import absolute_import, division, print_function
from six.moves import map, range, zip
import six

# built-in
import collections
import copy
import keyword
import os
import re

# 3rd party
import numpy as np

def MAD(data):
    '''computes median absolute deviation
    Function that compute median absolute deviation of data slice
    See: https://en.wikipedia.org/wiki/Median_absolute_deviation
    
    Parameters
    ----------
    data : 1-dimensional numpy array or list
        sequence containing data over which to compute the MAD
    Returns
    -------
    out : float
        the Median Absolute Deviation as computed
    Examples
    --------
    >>> x = [2, 4, 3, 4, 6, 7, 35, 2, 3, 4]
    >>> MAD(x)
    1.5
    '''
    med = np.median(data)
    return np.median(np.abs(data - med))

def normpath(path):
    """Normalize a path.

    Parameters
    ----------
    path : str
        The path to normalize.

    Returns
    -------
    npath : str
        The normalized path.

    """

    if '~' in path:
        out = os.path.abspath(os.path.expanduser(path))
    else:
        out = os.path.abspath(path)

    return out


def fileparts(path):
    """split a file path into its directory, name, and extension.
    
    Parameters
    ----------
    path : str
        Input file path.
    
    Returns
    -------
    dirname : str
        File directory.
    fname : str
        File name.
    ext : str
        File extension.
    
    Notes
    -----
    * Removes the dot ('.') from the extension.
    
    """
    
    dirname, fname = os.path.split(path)
    fname, ext = os.path.splitext(fname)
    ext = ext.replace('.', '')
    
    return dirname, fname, ext


def fullfile(*args):
    """Join one or more file path components, assuming the last is
    the extension.
    
    Parameters
    ----------
    ``*args`` : list, optional
        Components to concatenate.
    
    Returns
    -------
    fpath : str
        The concatenated file path.
    
    """
    
    nb = len(args)
    if nb == 0:
        return ''
    elif nb == 1:
        return args[0]
    elif nb == 2:
        return os.path.join(*args)
    
    fpath = os.path.join(*args[:-1]) + '.' + args[-1]
    
    return fpath


def walktree(top=None, spec=None):
    """Iterator to recursively descend a directory and return all files
    matching the spec.
    
    Parameters
    ----------
    top : str, optional
        Starting directory; if None, defaults to the current working directoty.
    spec : str, optional
        Regular expression to match the desired files;
        if None, matches all files; typical patterns:
            * `r'\.txt$'` - matches files with '.txt' extension;
            * `r'^File_'` - matches files starting with 'File\_'
            * `r'^File_.+\.txt$'` - matches files starting with 'File\_' and ending with the '.txt' extension.
    
    Yields
    ------
    fpath : str
        Absolute file path.
    
    Notes
    -----
    * Partial matches are also selected.
    
    See Also
    --------
    * https://docs.python.org/3/library/re.html
    * https://regex101.com/
    
    """
    
    if top is None:
        top = os.getcwd()
    
    if spec is None:
        spec = r'.*?'
    
    prog = re.compile(spec)
    
    for root, _, files in os.walk(top):
        for name in files:
            if prog.search(name):
                fname = os.path.join(root, name)
                yield fname


def remainderAllocator(votes, k, reverse=True, check=False):
    """Allocate k seats proportionally using the Remainder Method.

    Also known as Hare-Niemeyer Method. Uses the Hare quota.

    Parameters
    ----------
    votes : list
        Number of votes for each class/party/cardinal.
    k : int
        Total number o seats to allocate.
    reverse : bool, optional
        If True, allocates remaining seats largest quota first.
    check : bool, optional
        If True, limits the number of seats to the total number of votes.

    Returns
    -------
    seats : list
        Number of seats for each class/party/cardinal.

    """

    # check total number of votes
    tot = np.sum(votes)
    if check and k > tot:
        k = tot

    # frequencies
    length = len(votes)
    freqs = np.array(votes, dtype='float') / tot

    # assign items
    aux = k * freqs
    seats = aux.astype('int')

    # leftovers
    nb = k - seats.sum()
    if nb > 0:
        if reverse:
            ind = np.argsort(aux - seats)[::-1]
        else:
            ind = np.argsort(aux - seats)

        for i in range(nb):
            seats[ind[i % length]] += 1

    return seats.tolist()


def highestAveragesAllocator(votes, k, divisor='dHondt', check=False):
    """Allocate k seats proportionally using the Highest Averages Method.

    Parameters
    ----------
    votes : list
        Number of votes for each class/party/cardinal.
    k : int
        Total number o seats to allocate.
    divisor : str, optional
        Divisor method; one of 'dHondt', 'Huntington-Hill', 'Sainte-Lague',
        'Imperiali', or 'Danish'.
    check : bool, optional
        If True, limits the number of seats to the total number of votes.

    Returns
    -------
    seats : list
        Number of seats for each class/party/cardinal.

    """

    # check total number of cardinals
    tot = np.sum(votes)
    if check and k > tot:
        k = tot

    # select divisor
    if divisor == 'dHondt':
        fcn = lambda i: float(i)
    elif divisor == 'Huntington-Hill':
        fcn = lambda i: np.sqrt(i * (i + 1.))
    elif divisor == 'Sainte-Lague':
        fcn = lambda i: i - 0.5
    elif divisor == 'Imperiali':
        fcn = lambda i: float(i + 1)
    elif divisor == 'Danish':
        fcn = lambda i: 3. * (i - 1.) + 1.
    else:
        raise ValueError("Unknown divisor method.")

    # compute coefficients
    tab = []
    length = len(votes)
    D = [fcn(i) for i in range(1, k + 1)]
    for i in range(length):
        for j in range(k):
            tab.append((i, votes[i] / D[j]))

    # sort
    tab.sort(key=lambda item: item[1], reverse=True)
    tab = tab[:k]
    tab = np.array([item[0] for item in tab], dtype='int')

    seats = np.zeros(length, dtype='int')
    for i in range(length):
        seats[i] = np.sum(tab == i)

    return seats.tolist()


def random_fraction(indx, fraction, sort=True):
    """Select a random fraction of an input list of elements.

    Parameters
    ----------
    indx : list, array
        Elements to partition.
    fraction : int, float
        Fraction to select.
    sort : bool, optional
        If True, output lists will be sorted.

    Returns
    -------
    use : list, array
        Selected elements.
    unuse : list, array
        Remaining elements.

    """

    # number of elements to use
    fraction = float(fraction)
    nb = int(fraction * len(indx))

    # copy because shuffle works in place
    aux = copy.deepcopy(indx)

    # shuffle
    np.random.shuffle(indx)

    # select
    use = aux[:nb]
    unuse = aux[nb:]

    # sort
    if sort:
        use.sort()
        unuse.sort()

    return use, unuse


class ReturnTuple(tuple):
    """A named tuple to use as a hybrid tuple-dict return object.

    Parameters
    ----------
    values : iterable
        Return values.
    names : iterable, optional
        Names for return values.

    Raises
    ------
    ValueError
        If the number of values differs from the number of names.
    ValueError
        If any of the items in names:
        * contain non-alphanumeric characters;
        * are Python keywords;
        * start with a number;
        * are duplicates.

    """

    def __new__(cls, values, names=None):

        return tuple.__new__(cls, tuple(values))

    def __init__(self, values, names=None):

        nargs = len(values)

        if names is None:
            # create names
            names = ['_%d' % i for i in range(nargs)]
        else:
            # check length
            if len(names) != nargs:
                raise ValueError("Number of names and values mismatch.")

            # convert to str
            names = list(map(str, names))

            # check for keywords, alphanumeric, digits, repeats
            seen = set()
            for name in names:
                if not all(c.isalnum() or (c == '_') for c in name):
                    raise ValueError("Names can only contain alphanumeric \
                                      characters and underscores: %r." % name)

                if keyword.iskeyword(name):
                    raise ValueError("Names cannot be a keyword: %r." % name)

                if name[0].isdigit():
                    raise ValueError("Names cannot start with a number: %r." %
                                     name)

                if name in seen:
                    raise ValueError("Encountered duplicate name: %r." % name)

                seen.add(name)

        self._names = names

    def as_dict(self):
        """Convert to an ordered dictionary.

        Returns
        -------
        out : OrderedDict
            An OrderedDict representing the return values.

        """

        return collections.OrderedDict(zip(self._names, self))

    __dict__ = property(as_dict)

    def __getitem__(self, key):
        """Get item as an index or keyword.

        Returns
        -------
        out : object
            The object corresponding to the key, if it exists.

        Raises
        ------
        KeyError
            If the key is a string and it does not exist in the mapping.
        IndexError
            If the key is an int and it is out of range.

        """

        if isinstance(key, six.string_types):
            if key not in self._names:
                raise KeyError("Unknown key: %r." % key)

            key = self._names.index(key)

        return super(ReturnTuple, self).__getitem__(key)

    def __repr__(self):
        """Return representation string."""

        tpl = '%s=%r'

        rp = ', '.join(tpl % item for item in zip(self._names, self))

        return 'ReturnTuple(%s)' % rp

    def __getnewargs__(self):
        """Return self as a plain tuple; used for copy and pickle."""

        return tuple(self)

    def keys(self):
        """Return the value names.

        Returns
        -------
        out : list
            The keys in the mapping.

        """

        return list(self._names)
